package br.ufrj.dcc.dm.action;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import br.ufrj.dcc.dm.model.AttributeMetadata;
import br.ufrj.dcc.dm.model.Entry;
import br.ufrj.dcc.dm.model.decisionTree.ClassNode;
import br.ufrj.dcc.dm.model.decisionTree.Decision;
import br.ufrj.dcc.dm.model.decisionTree.Node;
import br.ufrj.dcc.dm.model.decisionTree.Tree;
import br.ufrj.dcc.dm.util.ArffDatasetReader;
import br.ufrj.dcc.dm.util.AttributeFilter;

public class DecisionTreeBuilder
{

	private static final Integer POSITIVE_DECISION_INT_INDEX = -1;
	private static final Integer NEGATIVE_DECISION_INT_INDEX = -2;
	
	private ArrayList<Entry> entries;

	private String datasetPath;
	
	private Tree decisionTree;
	
	private ArffDatasetReader ds;
	
	private Map<Integer, AttributeMetadata> attributeMetadataMap;
	
	private EntropyCalc ec;
	
	private Set<Integer> availableIndexes;
	
	public DecisionTreeBuilder(String datasetPath)
	{
		this.datasetPath = datasetPath;
	}
	
	public DecisionTreeBuilder(ArrayList<Entry> entries) 
	{
		this.entries = entries;
	}

	public void prePopulate()
	{
		decisionTree = new Tree();
		
		ds = new ArffDatasetReader(datasetPath);
		entries = ds.assembleEntries();
		attributeMetadataMap = ds.retrieveAttributeMetadataMap();
		
		availableIndexes = new HashSet<Integer>();
		
		for(int i = 0; i < attributeMetadataMap.size() - 1; i ++)
		{
			availableIndexes.add(i);
		}
	}
	
	public void buildDecisionTree()
	{
		if(decisionTree == null)
		{
			prePopulate();
		}
		
		if(decisionTree.getRootNode() == null)
		{
			int rootAttributeIndex = decideRootAttribute();
			
			availableIndexes.remove(rootAttributeIndex);

			decisionTree.setRootNode(assembleNodes(entries, rootAttributeIndex));
		}
		
		

	}
	
	private Node assembleNodes(List<Entry> entries, int attributeIndex)
	{
		Node node = new Node();
		node.setAttributeIndex(attributeIndex);
		
		String[] possibleValues = attributeMetadataMap.get(attributeIndex).getPossibleValues();
		Integer nextIndex;
		
		if(availableIndexes.isEmpty())
		{
			return null;
		}
		
		for(String value : possibleValues)
		{
			List<Entry> filteredEntries = AttributeFilter.retrieveFilteredEntries(entries, attributeIndex, value);
			nextIndex = decideNextAttribute(filteredEntries, attributeIndex);
			if(nextIndex == NEGATIVE_DECISION_INT_INDEX)
			{
				node.getDecisions().add(new Decision(value, new ClassNode("No")));
			}
			else
			{
				if(nextIndex == POSITIVE_DECISION_INT_INDEX)
				{
					node.getDecisions().add(new Decision(value, new ClassNode("Yes")));
				}
				else
				{
					availableIndexes.remove(attributeIndex);
					Node decidedNode = assembleNodes(filteredEntries, nextIndex);
					node.getDecisions().add(new Decision(value, decidedNode));
				}
			}
			
		}
		
		return node;
	}
	
	private int decideRootAttribute()
	{
		ec = new EntropyCalc(entries);
		
		Double rootEntropy = ec.calculateRootEntropy();
		
		
		System.out.println("Dataset Root Entropy: " + rootEntropy);
		System.out.println("________________________________________________________________");
		
		int bestGainAttributeIndex = 0;
		double bestGainValue = 0;
		
		for(int i = 0; i < attributeMetadataMap.size() - 1; i ++)
		{
			System.out.println("Attribute \"" + attributeMetadataMap.get(i).getAttributeName() + "\"");
			Double attributeEntropy = ec.calculateAttributeEntropy(i);
			Double attributeGain = GainCalc.calculateGain(rootEntropy, attributeEntropy);
			System.out.println("Entropy: " + attributeEntropy);
			System.out.println("Gain: " + attributeGain);
			System.out.println("________________________________________________________________");
			
			if(attributeGain > bestGainValue)
			{
				bestGainValue = attributeGain;
				bestGainAttributeIndex = i;
			}
		}
		
		System.out.println("Best attribute (Higher gain): Attribute \"" + attributeMetadataMap.get(bestGainAttributeIndex).getAttributeName() + "\" with a gain of "+ bestGainValue );
		
		return bestGainAttributeIndex;
	}
	
	private Integer decideNextAttribute(List<Entry> paramEntries, int debugIndex)
	{
		EntropyCalc entropyCalc = new EntropyCalc((ArrayList<Entry>) paramEntries);
		
		Double rootEntropy = entropyCalc.calculateRootEntropy();
		
		/**
		 * TODO: Please fix this way of doing things.
		 */
		if(rootEntropy == Double.NEGATIVE_INFINITY)
		{
			return NEGATIVE_DECISION_INT_INDEX;
		}
		
		if(rootEntropy == Double.POSITIVE_INFINITY)
		{
			return POSITIVE_DECISION_INT_INDEX;
		}
		
		System.out.println("Dataset Root Entropy: " + rootEntropy);
		System.out.println("________________________________________________________________");
		
		int bestGainAttributeIndex = 0;
		double bestGainValue = 0;
		
		for(int i = 0; i < attributeMetadataMap.size() - 1; i ++)
		{
			if(!availableIndexes.contains(i))
			{
				System.out.println("Dropping for " + i);
				continue;
			}
				
			
			System.out.println("Attribute \"" + attributeMetadataMap.get(i).getAttributeName() + "\"");
			Double attributeEntropy = entropyCalc.calculateAttributeEntropy(i);
			Double attributeGain = GainCalc.calculateGain(rootEntropy, attributeEntropy);
			System.out.println("Entropy: " + attributeEntropy);
			System.out.println("Gain: " + attributeGain);
			System.out.println("________________________________________________________________");
			
			if(attributeGain > bestGainValue)
			{
				bestGainValue = attributeGain;
				bestGainAttributeIndex = i;
			}
		}
		
		System.out.println("Best attribute (Higher gain for " + attributeMetadataMap.get(debugIndex).getAttributeName() +"): Attribute \"" + attributeMetadataMap.get(bestGainAttributeIndex).getAttributeName() + "\" with a gain of "+ bestGainValue );
		
		return bestGainAttributeIndex;
	}
	
	public void printDecisionTree(Node node)
	{
		String attributeName = attributeMetadataMap.get(node.getAttributeIndex()).getAttributeName();
		System.out.println("Deciding to node \"" + attributeName + "\"");
		
		for(Decision decision : node.getDecisions())
		{
			Node decidedNode = decision.getDecidedNode();
			
			if(decidedNode instanceof ClassNode)
			{
				ClassNode classNode = (ClassNode) decidedNode;
				System.out.println("[FROM " + attributeName + "] [LEAF] " + decision.getValue() + " decides to: " + classNode.getDecisionClassValue());
			}
			else
			{
				System.out.println("[FROM " + attributeName + "] " + decision.getValue() + " decides to: " + attributeMetadataMap.get(decidedNode.getAttributeIndex()).getAttributeName());
				printDecisionTree(decidedNode);
			}
			
		}
	}
	
	public ArrayList<Entry> getEntries() 
	{
		return entries;
	}

	public void setEntries(ArrayList<Entry> entries) 
	{
		this.entries = entries;
	}

	public String getDatasetPath()
	{
		return datasetPath;
	}

	public void setDatasetPath(String datasetPath)
	{
		this.datasetPath = datasetPath;
	}

	public Map<Integer, AttributeMetadata> getAttributeMetadataMap()
	{
		return attributeMetadataMap;
	}

	public void setAttributeMetadataMap(Map<Integer, AttributeMetadata> attributeMetadataMap) 
	{
		this.attributeMetadataMap = attributeMetadataMap;
	}

	public Tree getDecisionTree() 
	{
		return decisionTree;
	}

	public void setDecisionTree(Tree decisionTree) 
	{
		this.decisionTree = decisionTree;
	}
	
}
